/**
Copyright (c) 2008 New Gonzo <john@newgonzo.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
**/


﻿package com.newgonzo.cannonball.dom
{
	import com.newgonzo.cannonball.w3c.dom.IDocument;
	import com.newgonzo.cannonball.w3c.dom.IDOMImplementation;
	import com.newgonzo.cannonball.w3c.dom.IElement;
	import com.newgonzo.cannonball.w3c.dom.INode;
	import com.newgonzo.cannonball.w3c.dom.DocumentPosition;
	import com.newgonzo.cannonball.w3c.dom.views.IDocumentView;
	import com.newgonzo.cannonball.w3c.dom.views.IAbstractView;
	import com.newgonzo.cannonball.w3c.dom.traversal.ITreeWalker;
	import com.newgonzo.cannonball.w3c.dom.traversal.INodeFilter;
	import com.newgonzo.cannonball.w3c.dom.traversal.IDocumentTraversal;
	import com.newgonzo.cannonball.dom.traversal.TreeWalker;
	
	import flash.utils.Dictionary;
	import flash.events.EventDispatcher;

	import com.adobe.net.URI;
	
	import memorphic.xpath.XPathQuery;
	
	public class Document extends Node implements IExtendedDocument, IDocumentTraversal, IDocumentView
	{	
		private var impl:IDOMImplementation;
		private var docView:IAbstractView;
		
		private var elementCache:Dictionary;
		private var docUri:String = "about:blank";
		
		private var nodePositionCache:Dictionary = new Dictionary(true);
		
		public function Document(implementation:IDOMImplementation = null, defaultView:IAbstractView = null)
		{
			super();
			
			impl = implementation;
			docView = defaultView;
			elementCache = new Dictionary();
		}
		
		public function get implementation():IDOMImplementation
		{
			if(!impl)
			{
				impl = new DOMImplementation();
			}
			
			return impl;
		}
		
		public function get view():IAbstractView
		{
			return docView;
		}
		
		public function set view(value:IAbstractView):void
		{
			docView = value;
		}
		
		public function get defaultView():IAbstractView
		{
			return view;
		}
		
		public function get documentElement():IElement
		{
			return getElement(content as XML);
		}
		
		public function set documentURI(value:String):void
		{
			docUri = value;
		}
		
		public function get documentURI():String
		{
			return docUri;
		}
		
		public function resolve(path:String):String
		{
			// check for #
			if(path.search(/^\s?#/) != -1)
			{
				return path;
			}
			
			var uri:URI = new URI(path);
			
			if(documentURI)
			{
				uri.makeAbsoluteURI(new URI(documentURI));
			}
			
			return uri.toString();
		}
		
		public function createTreeWalker(rootNode:INode, whatToShow:int = -1, filter:INodeFilter = null):ITreeWalker
		{
			return new TreeWalker(rootNode, whatToShow, filter);
		}
		
		public function createElement(tagName:String):IElement
		{
			return new Element();
		}
	
		public function getElement(node:XML):IElement
		{
			if(!node || node.nodeKind() != "element") return null;
			
			var key:String = getElementKey(node);
			var element:IExtendedElement = elementCache[key] as IExtendedElement;

			if(!element)
			{
				element = createElement(node.localName().toString()) as IExtendedElement;
			
				if(!element)
				{
					throw new DOMError("Document.createElement must return an element of type IExtendedElement.");
				}
				
				element.document = this;
				element.content = node;
				
				elementCache[key] = element;
			}
			
			return element;
		}
		
		public function getElements(nodes:XMLList):Array
		{
			if(!nodes) return null;
			
			var a:Array = new Array();
			var node:XML;
			var elem:IElement;
			
			for each(node in nodes)
			{
				elem = getElement(node);
				
				if(elem)
				{
					a.push(elem);
				}
			}
		
			return a;
		}
		
		public function getElementKey(node:XML):String
		{
			if(node == null) throw new DOMError("getElementKey: node cannot be null.");
			
			var key:String = node.childIndex().toString();	
			var p:XML = node.parent();

			// see memorphic.xpath.parser.QueryRoot
			while(p && p.parent() && p.parent().localName() != "xml-document")
			{
				key = p.childIndex().toString() + "-" + key;
				p = p.parent();
			}

			return key;
		}
		
		public function compareNodePositions(node1:INode, node2:INode):uint
		{
			var node1Positions:Dictionary = getNodePositions(node1);
			var node2Positions:Dictionary = getNodePositions(node2);
			
			if(node1Positions[node2] != null)
			{
				trace("CACHED!!!!")
				return node1Positions[node2] as uint;
			}
			
			if(node2Positions[node1] != null)
			{
				trace("INVERSE CACHED!!!!");
				return node2Positions[node1] as uint;
			}
			
			var pos:uint = DocumentPosition.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
			var inv:uint = DocumentPosition.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
			
			var parents:Array = new Array();
			var nodeParents:Array = new Array();
			
			var parent:INode = node1.parentNode;
			var nodeParent:INode = node2.parentNode;
			
			// same parent? check position
			if(parent == nodeParent)
			{
				var parentChildren:Array = parent.childNodes;
				
				if(parentChildren.indexOf(node1) < parentChildren.indexOf(node2))
				{
					pos = DocumentPosition.DOCUMENT_POSITION_PRECEDING;
					inv = DocumentPosition.DOCUMENT_POSITION_FOLLOWING;
				}
				else
				{
					pos = DocumentPosition.DOCUMENT_POSITION_FOLLOWING;
					inv = DocumentPosition.DOCUMENT_POSITION_PRECEDING;
				}
			}
			else
			{
				var commonParent:INode;
				var containedBy:Boolean = false;
				var contains:Boolean = false;
				
				// collect ancestry
				while(parent)
				{
					parents.push(parent);
					
					if(parent == node2)
					{
						containedBy = true;
					}
					
					parent = parent.parentNode;
				}
				
				while(nodeParent)
				{
					if(!commonParent && parents.indexOf(nodeParent) != -1)
					{
						commonParent = nodeParent;
					}
					
					if(nodeParent == node1)
					{
						contains = true;
					}
					
					nodeParents.push(nodeParent);
					nodeParent = nodeParent.parentNode;
				}
				
				if(contains)
				{
					pos = DocumentPosition.DOCUMENT_POSITION_CONTAINS;
					inv =  DocumentPosition.DOCUMENT_POSITION_CONTAINED_BY;
				}
				else if(containedBy)
				{
					pos =  DocumentPosition.DOCUMENT_POSITION_CONTAINED_BY;
					inv = DocumentPosition.DOCUMENT_POSITION_CONTAINS;
				}
				else
				{
					// compare second-to-common parent
					var commonParentChild:INode = parents[parents.indexOf(commonParent) - 1];
					var nodeCommonParentChild:INode = nodeParents[nodeParents.indexOf(commonParent) - 1];
					
					if(!commonParentChild)
					{
						commonParentChild = node1;
					}
					
					if(!nodeCommonParentChild)
					{
						nodeCommonParentChild = node2;
					}
					
					if(commonParent.childNodes.indexOf(commonParentChild) < commonParent.childNodes.indexOf(nodeCommonParentChild))
					{
						pos = DocumentPosition.DOCUMENT_POSITION_PRECEDING;
						inv = DocumentPosition.DOCUMENT_POSITION_FOLLOWING;
					}
					else
					{
						pos = DocumentPosition.DOCUMENT_POSITION_FOLLOWING;
						inv = DocumentPosition.DOCUMENT_POSITION_PRECEDING;
					}
				}
			}
			
			node1Positions[node2] = pos;
			node2Positions[node1] = inv;
			
			return pos;
		}
		
		public function evaluate(xpath:String, context:INode = null):Array
		{
			var xml:XML = XML(context ? (context as IExtendedNode).content : this.content);
			
			var a:Array = new Array();
			var node:XML;
			
			var result:* = XPathQuery.execQuery(xml, xpath);
			
			for each(node in result)
			{
				a.push(getElement(node));
			}
			
			return a;
		}
		
		override public function toString():String
		{
			return "[Document]";
		}
		
		private function getNodePositions(node:INode):Dictionary
		{
			var dict:Dictionary;
			
			if(nodePositionCache[node] != null)
			{
				dict = nodePositionCache[node] as Dictionary;
			}
			else
			{
				dict = new Dictionary(true);
				nodePositionCache[node] = dict;
			}
			
			return dict;
		}
	}
}